/*
 *  soapUI, copyright (C) 2006 eviware.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of the GNU Lesser General Public License as published by the Free Software Foundation; 
 *  either version 2.1 of the License, or (at your option) any later version.
 *
 *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 *  See the GNU Lesser General Public License for more details at gnu.org.
 */

package at.ac.tuwien.infosys.dsg.daios.wsdl.parser.soapui;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Logger;

import javax.xml.namespace.QName;

import org.apache.xmlbeans.SchemaAnnotation;
import org.apache.xmlbeans.SchemaLocalElement;
import org.apache.xmlbeans.SchemaParticle;
import org.apache.xmlbeans.SchemaType;
import org.apache.xmlbeans.SchemaTypeSystem;
import org.apache.xmlbeans.SimpleValue;
import org.apache.xmlbeans.XmlAnySimpleType;
import org.apache.xmlbeans.XmlBase64Binary;
import org.apache.xmlbeans.XmlBeans;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlHexBinary;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * XML-Schema related tools
 * 
 * @author Ole.Matzura
 */

public class SchemaUtils
{
   private final static Logger log = Logger.getLogger("Daios");
   private static Map<String,XmlObject> defaultSchemas = new HashMap<String,XmlObject>();
	
   public static SchemaTypeSystem loadSchemaTypes(String wsdlUrl, SoapVersion soapVersion, WsdlLoader loader ) throws SchemaException
   {
   	ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
   	Thread.currentThread().setContextClassLoader( ClassLoader.getSystemClassLoader() );
   	
   	try
		{
			log.info("Loading schema types from [" + wsdlUrl + "]");
			ArrayList<XmlObject> schemas = new ArrayList<XmlObject>(getSchemas(
					wsdlUrl, loader).values());

			return buildSchemaTypes(schemas, soapVersion);
		}
		catch (Exception e)
		{
			throw new SchemaException( "Error loading schema types", e );
		}
		finally
   	{
   		Thread.currentThread().setContextClassLoader( contextClassLoader );
   	}
   }

   public static SchemaTypeSystem buildSchemaTypes(List<XmlObject> schemas, SoapVersion soapVersion) throws SchemaException
   {
      XmlOptions options = new XmlOptions();
      options.setCompileNoValidation();
      options.setCompileNoPvrRule();
      options.setCompileDownloadUrls();
      options.setCompileNoUpaRule();
      options.setValidateTreatLaxAsSkip();
      
      ArrayList errorList = new ArrayList();
      options.setErrorListener( errorList );

      try
		{
      	// remove imports
			for( int c = 0; c < schemas.size(); c++ )
			{
				XmlObject s = schemas.get( c );
				
				Map map = new HashMap();
				XmlCursor cursor = s.newCursor();
				cursor.toStartDoc(); 
				if( toNextContainer(cursor))
					cursor.getAllNamespaces( map );
				else
					log.severe( "Can not get namespaces for " + s );
				
				String tns = getTargetNamespace(s);
				
				log.info( "schema for [" + tns + "] contained [" + map.toString() + "] namespaces" );

				
				if( defaultSchemas.containsKey( tns ) ||
					 tns.equals( getTargetNamespace(soapVersion.getSoapEncodingSchema()) ) ||
					 tns.equals( getTargetNamespace(soapVersion.getSoapEnvelopeSchema() )))
				{
					schemas.remove( c );
					c--;
				}
				else
				{
					removeImports(s);
				}
			}

      	schemas.add( soapVersion.getSoapEncodingSchema());
      	schemas.add( soapVersion.getSoapEnvelopeSchema());
      	schemas.addAll( defaultSchemas.values() );
      	
      	SchemaTypeSystem sts = XmlBeans.compileXsd(
      			schemas.toArray(new XmlObject[schemas.size()]), XmlBeans.getBuiltinTypeSystem(), options);
      	return sts;
//      	return XmlBeans.typeLoaderUnion(new SchemaTypeLoader[] { sts, XmlBeans.getBuiltinTypeSystem() });
		}
		catch (Exception e)
		{
			e.printStackTrace();
			throw new SchemaException( e, errorList );
		}
		finally
		{
			for( int c = 0; c < errorList.size(); c++ )
			{
				log.severe( "Error: " + errorList.get( c ));
			}
		}
   }

	public static boolean toNextContainer(XmlCursor cursor)
	{
		while( !cursor.isContainer() && !cursor.isEnddoc() )
			cursor.toNextToken();
		
		return cursor.isContainer();
	}

	public static String getTargetNamespace(XmlObject s)
	{
		return ((Document)s.getDomNode()).getDocumentElement().getAttribute( "targetNamespace" );
	}
   
   public static Map<String,XmlObject> getSchemas( String wsdlUrl, WsdlLoader loader ) throws SchemaException
   {
   	Map<String,XmlObject> result = new HashMap<String,XmlObject>();
   	getSchemas( wsdlUrl, result, loader );
   	return result;
   }
   
   /**
    * Returns a map mapping urls to corresponding XmlSchema XmlObjects for the specified wsdlUrl
    */
   
   public static void getSchemas( String wsdlUrl, Map<String,XmlObject> existing,  WsdlLoader loader ) throws SchemaException
   {
   	if( existing.containsKey( wsdlUrl ))
   		return; 
   	
   	log.info( "Getting schema " + wsdlUrl );
   	
   	ArrayList errorList = new ArrayList();
   	
   	Map<String,XmlObject> result = new HashMap<String,XmlObject>();
   	
      try
		{
			XmlOptions options = new XmlOptions();
			options.setCompileNoValidation();
			options.setSaveUseOpenFrag();
			options.setErrorListener( errorList );
			options.setSaveSyntheticDocumentElement(new QName( Constants.XSD_NS, "schema"));

			XmlObject xmlObject = loader.loadXmlObject(wsdlUrl, options);

			Document dom = (Document) xmlObject.getDomNode();
			Node domNode = dom.getDocumentElement();
			if (domNode.getLocalName().equals("schema")
					&& domNode.getNamespaceURI().equals(
							Constants.XSD_NS))
			{
				result.put(wsdlUrl, xmlObject);
			}
			else
			{
				XmlObject[] schemas = xmlObject
						.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:schema");

				for (int i = 0; i < schemas.length; i++)
				{
					XmlCursor xmlCursor = schemas[i].newCursor();
					String xmlText = xmlCursor.getObject().xmlText(options);
					schemas[i] = XmlObject.Factory.parse(xmlText, options);
					schemas[i].documentProperties().setSourceName(wsdlUrl);
					
					result.put(wsdlUrl + "@" + (i+1), schemas[i]);
				}

				XmlObject[] wsdlImports = xmlObject
						.selectPath("declare namespace s='" + Constants.WSDL11_NS + "' .//s:import/@location");
				for (int i = 0; i < wsdlImports.length; i++)
				{
					String location = ((SimpleValue) wsdlImports[i]).getStringValue();
					if (location != null)
					{
						if (location.startsWith("file:")
								|| location.indexOf("://") > 0)
						{
							getSchemas(location, existing, loader);
						}
						else
						{
							getSchemas(Tools.joinRelativeUrl(wsdlUrl, location), existing, loader);
						}
					}
				}
			}

			existing.putAll( result );
			
			XmlObject[] schemas = result.values().toArray(
					new XmlObject[result.size()]);

			for (int c = 0; c < schemas.length; c++)
			{
				xmlObject = schemas[c];

				XmlObject[] schemaImports = xmlObject
						.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:import/@schemaLocation");
				for (int i = 0; i < schemaImports.length; i++)
				{
					String location = ((SimpleValue) schemaImports[i])
							.getStringValue();
					if (location != null)
					{
						if (location.startsWith("file:")
								|| location.indexOf("://") > 0)
						{
							getSchemas(location, existing, loader);
						}
						else
						{
							getSchemas(Tools.joinRelativeUrl(wsdlUrl, location), existing, loader);
						}
					}
				}

				XmlObject[] schemaIncludes = xmlObject
						.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:include/@schemaLocation");
				for (int i = 0; i < schemaIncludes.length; i++)
				{
					String location = ((SimpleValue) schemaIncludes[i])
							.getStringValue();
					if (location != null)
					{
						if (location.startsWith("file:")
								|| location.indexOf("://") > 0)
						{
							getSchemas(location, existing, loader);
						}
						else
						{
							getSchemas(Tools.joinRelativeUrl(wsdlUrl, location), existing, loader);
						}
					}
				}
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
			throw new SchemaException( e, errorList );
		}
   }
   
   /**
    * Returns a map mapping urls to corresponding XmlObjects for the specified wsdlUrl
    */
   
   public static Map<String,XmlObject> getDefinitionParts( WsdlLoader loader ) throws Exception
   {
   	HashMap<String, XmlObject> result = new HashMap<String,XmlObject>();
		getDefinitionParts( loader.getBaseURI(), result, loader );
		return result;
   }
   
   public static void getDefinitionParts( String wsdlUrl, Map<String,XmlObject> existing, WsdlLoader loader ) throws Exception
   {
   	if( existing.containsKey( wsdlUrl ))
   		return;
   	
      XmlObject xmlObject = loader.loadXmlObject( wsdlUrl, null );
      existing.put( wsdlUrl, xmlObject );

    	XmlObject [] wsdlImports = xmlObject.selectPath("declare namespace s='" + Constants.WSDL11_NS + "' .//s:import");
      for (int i = 0; i < wsdlImports.length; i++)
      {
         String location = wsdlImports[i].getDomNode().getAttributes().getNamedItem( "location" ).getNodeValue();
         if( location != null )
         {
            if( location.startsWith( "file:" ) || location.indexOf( "://") > 0 )
            {
            	getDefinitionParts( location, existing, loader );
            }
            else
            {
            	getDefinitionParts( Tools.joinRelativeUrl( wsdlUrl, location ), existing, loader );
            }
         }
      }
      
      XmlObject[] schemaImports = xmlObject.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:import/@schemaLocation");
      for (int i = 0; i < schemaImports.length; i++)
      {
         String location = ((SimpleValue)schemaImports[i]).getStringValue();
         if( location != null )
         {
            if( location.startsWith( "file:" ) || location.indexOf( "://") > 0 )
            {
            	getDefinitionParts( location, existing, loader );
            }
            else
            {
            	getDefinitionParts( Tools.joinRelativeUrl( wsdlUrl, location ), existing, loader );
            }
         }
      }
      
      XmlObject[] schemaIncludes = xmlObject.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:include/@schemaLocation");
      for (int i = 0; i < schemaIncludes.length; i++)
      {
         String location = ((SimpleValue)schemaIncludes[i]).getStringValue();
         if( location != null  )
         {
            if( location.startsWith( "file:" ) || location.indexOf( "://") > 0 )
            {
            	getDefinitionParts( location, existing, loader );
            }
            else
            {
            	getDefinitionParts( Tools.joinRelativeUrl( wsdlUrl, location ), existing, loader );
            }
         }
      }
   }
   
   /**
    * Extracts namespaces - used in tool integrations for mapping..
    */

	public static Collection<String> extractNamespaces( SchemaTypeSystem schemaTypes )
	{
		Set<String> namespaces = new HashSet<String>();
		SchemaType[] globalTypes = schemaTypes.globalTypes();
		for( int c = 0; c < globalTypes.length; c++ )
		{
			namespaces.add( globalTypes[c].getName().getNamespaceURI() );
		}
		
		namespaces.remove( Constants.SOAP11_ENVELOPE_NS );
		namespaces.remove( Constants.SOAP_ENCODING_NS );
		
		return namespaces;
	}
   
   /**
    * Used when creating a TypeSystem from a complete collection of SchemaDocuments so that referenced 
    * types are not downloaded (again)
    */
   
	public static void removeImports(XmlObject xmlObject) throws XmlException
	{
		 XmlObject[] imports = xmlObject
       	.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:import");
		 
		 for( int c = 0; c < imports.length; c++ )
		 {
			 XmlCursor cursor = imports[c].newCursor();
			 cursor.removeXml();
			 cursor.dispose();
		 }
		 
		 XmlObject[] includes = xmlObject
     		.selectPath("declare namespace s='" + Constants.XSD_NS + "' .//s:include");
		 
		 for( int c = 0; c < includes.length; c++ )
		 {
			 XmlCursor cursor = includes[c].newCursor();
			 cursor.removeXml();
			 cursor.dispose();
		 }
	}

	public static boolean isInstanceOf( SchemaType schemaType, SchemaType baseType )
	{
		if( schemaType == null )
			return false;
		return schemaType.equals(baseType) ? true : isInstanceOf( schemaType.getBaseType(), baseType );
	}

	public static boolean isBinaryType(SchemaType schemaType)
	{
		return isInstanceOf( schemaType, XmlHexBinary.type ) ||
				isInstanceOf( schemaType, XmlBase64Binary.type );
	}

	public static String getDocumentation( SchemaParticle particle, SchemaType schemaType )
	{
		String result = null;
		
		if( particle instanceof SchemaLocalElement )
		{
			SchemaAnnotation annotation = ((SchemaLocalElement)particle).getAnnotation();
			if( annotation != null )
			{
				XmlObject[] userInformation = annotation.getUserInformation();
				if( userInformation != null && userInformation.length > 0 )
				{
					result = userInformation[0].toString(); // XmlUtils.getElementText( ( Element ) userInformation[0].getDomNode());
				}
			}
		}
		
		if( schemaType != null && schemaType.getAnnotation() != null )
		{
			XmlObject[] userInformation = schemaType.getAnnotation().getUserInformation();
			if( userInformation != null && userInformation.length > 0 )
			{
				result = userInformation[0].toString(); // = XmlUtils.getElementText( ( Element ) userInformation[0].getDomNode());
			}
		}
		
		if( result != null )
		{
			result = result.replaceAll( "<br/>", "\n" );
			
			StringTokenizer st = new StringTokenizer( result, "\r\n" );
			StringBuffer buf = new StringBuffer( "<html><body>" );
			boolean added = false;
			
			while( st.hasMoreElements() )
			{
				if( added )
					buf.append( "<br>" );
				String str = st.nextToken().trim();
				if( str.length() > 0 )
				{
					buf.append( str );
					added = true;
				}
			}
			buf.append( "</body></html>" );
			result = buf.toString();
		}
		
		return result;
	}

	public static String [] getEnumerationValues( SchemaType schemaType, boolean addNull )
	{
		if( schemaType != null )
		{
			XmlAnySimpleType[] enumerationValues = schemaType.getEnumerationValues();
			if( enumerationValues != null && enumerationValues.length > 0 )
			{
				if( addNull )
				{
					String [] values = new String[enumerationValues.length+1];
					values[0] = null;
					
					for( int c = 1; c < values.length; c++ )
						values[c] = enumerationValues[c-1].getStringValue();
					
					return values;
				}
				else
				{
					String [] values = new String[enumerationValues.length];
					
					for( int c = 0; c < values.length; c++ )
						values[c] = enumerationValues[c].getStringValue();
					
					return values;
				}
			}
		}
		
		return new String[0];
	}
}
